﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Management.Automation;
using System.Management.Automation.Host;
using System.Management.Automation.Runspaces;
using System.Windows.Media;

using Au = Microsoft.Research.AuDotNet;
using System.Windows.Controls;
using System.Windows;
using System.Text.RegularExpressions;

namespace Sean
{
    internal class TabCompletion
    {
        private MainWindow mainwin;
        private PowerShellThread psthread;
        Brush fg = Brushes.Yellow;
        Brush bg = Brushes.Black;

        List<string> names;
        private int name_display_start;

        public TabCompletion(MainWindow mainwin, PowerShellThread psthread)
        {
            this.mainwin = mainwin;
            this.psthread = psthread;
            this.names = new List<string>();
            this.name_display_start = 0;
        }

        string last_buffer = null;
        string buffertail;
        /// <summary>
        /// Main starting point.  OnTab is called, and looks in mainwin.GetInputBuffer.
        /// 
        ///  * if the buffer is the same as last time, cycle through the completions
        ///  * if a new buffer:
        ///     - generate a completion list by calling "Sean-get-completions.ps1"
        ///     - when generated, carry on to OnTabPart2
        /// </summary>
        public void OnTab()
        {
            string buffer = mainwin.GetInputBuffer();
            int bufferpos = mainwin.GetInputBufferPos();

            buffertail = buffer.Substring(bufferpos);
            buffer = buffer.Substring(0, bufferpos);
            if (last_buffer != null && String.Compare(buffer, last_buffer, true) != 0)
                name_display_start = 0;
            last_buffer = buffer;
            PowerSean.Token[] tokens;
            try
            {
                tokens = PowerSean.Parser.Parse(buffer);
            }
            catch (Exception e)
            {
                MainWindow.StaticShriek("Parse error: " + e.Message);
                return;
            }

            PowerShellHelper.AddCommandsDelegate cmd = (PSCommand cmds) =>
            {
                cmds.AddCommand("Sean-get-completions");
                cmds.AddArgument(buffer);
                cmds.AddArgument(tokens);
            };
            psthread.ExecuteAsync(cmd, OnTabPart2, null, false);
        }

        /// <summary>
        /// Render the completions list in the TextBlock "mainwin.completions", limiting the
        /// number displayed so the buffer does not obliterate the console.
        /// </summary>
        /// <param name="psos">The list of completions generated by Sean-get-completions.ps1</param>
        public void OnTabPart2(ICollection<PSObject> psos)
        {
            if (psos == null)
            {
                MainWindow.StaticShriek("Sean-get-completions failed");
                return;
            }

            string linestart = null;
            names = new List<string>();
            int names_count = 0;
            foreach (var pso in psos)
            {
                if (pso != null)
                {
                    if (linestart == null)
                        linestart = pso.ToString();
                    else
                    {
                        names.Add(pso.ToString());
                        ++names_count;
                    }
                }
            }

            // Clear completions
            Clear();

            // Have linestart, names.  Do something only if at least one.
            if (names_count > 0)
            {
                // Compute common prefix
                string prefix = Au.Utils.CommonPrefix(names);

                // If no common prefix, don't kill user's typing.
                if (prefix == "")
                {
                    // xx This can happen because [System.Management.Automation.CommandCompletion]::CompleteInput
                    // returns some entries with ".\" in front of them.  Let's make a new list, stripping those, and regenerate prefix
                    // There may be other cases, but I don't want to generalize until I know what they are.
                    Regex re1 = new Regex("^" + Regex.Escape(".\\") + "(.*)");
                    string[] newnames = new string[names.Count];
                    for (int i = 0; i < names.Count; ++i)
                        newnames[i] = re1.Replace(names[i], "$1");
                    prefix = Au.Utils.CommonPrefix(newnames);
                }
                
                if (prefix == "$")
                {
                    // As above, but "${" ...
                    Regex re1 = new Regex("^" + Regex.Escape("${") + "(.*)");
                    string[] newnames = new string[names.Count];
                    for (int i = 0; i < names.Count; ++i)
                        newnames[i] = re1.Replace(names[i], "$$$1");
                    prefix = Au.Utils.CommonPrefix(newnames);
                }

                if (prefix == "")
                    MainWindow.StaticShriek("No prefix!");

                // Add a backtick quote if any of the suffices need it
                Regex re = new Regex("^" + Regex.Escape(prefix) + "[^a-zA-Z0-9_.:/\\-]");
                foreach(string n in names)
                    if (re.IsMatch(n))
                    {
                        prefix += '`';
                        break;
                    }

                // Display them if > 1
                if (names_count > 1)
                {
                    int lastslash = prefix.LastIndexOfAny("\\:.$".ToCharArray());
                    if (lastslash == -1)
                        RenderNames(names, "");
                    else
                        RenderNames(names, prefix.Substring(0, lastslash + 1));
                }
                else
                    mainwin.completions_scroller.Visibility = System.Windows.Visibility.Collapsed;

                // Add a trailing space if it's the only one, and doesn't end in /
                if (names_count == 1 && prefix.Substring(prefix.Length - 1, 1) != "\\")
                {
                    // And if it starts with ", close them.
                    // xx fix this to properly determine if this is an unclosed string
                    if (prefix.StartsWith("\""))
                        prefix += "\"";

                    prefix += " ";
                }

                mainwin.inputbox.Text = linestart + prefix + buffertail;
                mainwin.inputbox.SelectionStart = linestart.Length + prefix.Length;
            }
        }

        public void Clear()
        {
            mainwin.completions.Children.Clear();
            mainwin.completions_scroller.Visibility = System.Windows.Visibility.Collapsed;
        }

        void RenderNames(IEnumerable<string> names, string prefix)
        {
            Grid g = mainwin.completions;
            int rows = g.RowDefinitions.Count;
            int cols = g.ColumnDefinitions.Count;
            //mainwin.completions_prev.Visibility = (name_display_start > 0) ? Visibility.Visible : Visibility.Hidden;

            int nprinted = 0;
            int i = 0;
            bool reached_end = true;
            mainwin.completions_prefix.Text = prefix;
            foreach (string name in names)
            {
                if (i++ < name_display_start)
                    continue;

                string trimmed_name = name;
                if (name.StartsWith(prefix, StringComparison.CurrentCultureIgnoreCase))
                {
                    trimmed_name = name.Substring(prefix.Length);
                }

                TextBlock tb = new TextBlock();
                tb.Text = trimmed_name;

                int row = nprinted / cols;

                int col = nprinted - cols * row;
                tb.SetValue(Grid.RowProperty, row);
                tb.SetValue(Grid.ColumnProperty, col);
                tb.Focusable = true;
                //tb.TextTrimming = TextTrimming.CharacterEllipsis;
                tb.Margin = new Thickness(2,0,0,0);
                //tb.Background = ((row + col) % 2 == 0) ? g.Background : Brushes.Black;
                tb.HorizontalAlignment = HorizontalAlignment.Stretch;
                if (RIGHT_JUSTIFY_LONG_NAMES)
                {
                    tb.LayoutUpdated += new EventHandler(tb_LayoutUpdated);
                    fix_justifications = true;
                }
                else
                {
                    tb.TextTrimming = TextTrimming.CharacterEllipsis;
                }
                g.Children.Add(tb);

                //MainWindow.StaticDebugWrite("g[" + row + "," + col + "] = " + trimmed_name + "\n");

                ++nprinted;
                if (nprinted >= rows * cols)
                {
                    reached_end = false;
                    break;
                }
            }
            if (name_display_start == 0 && reached_end)
                mainwin.completions_next.Visibility = Visibility.Hidden;
            else {
                mainwin.completions_next.Visibility = Visibility.Visible;
                mainwin.completions_next.Text = reached_end ? "[TAB] back to start" : "[TAB] for more";
            }

            //  Update start point
            if (reached_end)
                name_display_start = 0;
            else
                name_display_start = i;

            mainwin.completions_scroller.Visibility = System.Windows.Visibility.Visible;
            Au.Utils.ScrollToEnd(mainwin.scroller, 0);
        }

        /// <summary>
        /// If true, names too long to fit in the tab-completion block
        /// are right justified, so the tails can be seen.
        /// </summary>
        private static bool RIGHT_JUSTIFY_LONG_NAMES = true;
        private bool fix_justifications = false;

        // Called only if RIGHT_JUSTIFY_LONG_NAMES 
        void tb_LayoutUpdated(object sender, EventArgs e)
        {
            if (!fix_justifications)
                return;

            fix_justifications = false;

            Grid g = mainwin.completions;
            double columnwidth = g.ColumnDefinitions[0].ActualWidth;

            foreach(object child in g.Children)  {
                TextBlock tb = child as TextBlock;
                if (tb != null)
                {
                    if (tb.ActualWidth > columnwidth)
                        tb.HorizontalAlignment = HorizontalAlignment.Right;
                }
            }
            
        }

    }
}
